package com.wgo.bpot.server.service;

import java.util.Collection;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.Queue;
import java.util.concurrent.ConcurrentLinkedQueue;

import org.apache.log4j.Logger;

import com.wgo.bpot.common.transport.exception.db.DatabaseException;
import com.wgo.bpot.common.transport.servicefacade.DispatchEntry;
import com.wgo.bpot.common.transport.servicefacade.DispatchedModifyingMethodsTransport;
import com.wgo.bpot.common.transport.util.ReflectionHelper;
import com.wgo.bpot.wiring.common.DomainModelConvention;


/**
 * The purpose of this class is to retain the state whenever a client is saving data,
 * in order to make sure that the timestamp the client has on the objects it has read from the server
 * is after the last save to that particular object.
 *
 */
public class ModifyingOperationsState {
	private static final transient Logger log = Logger.getLogger(ModifyingOperationsState.class);
	private long serverStartTime;
	private Map<String,Long> modifyingMethods = new HashMap<String,Long>();
	private Queue<String> modifyingMethodsQueue = new ConcurrentLinkedQueue<String>();
	private int maxOperationsToKeep = 10000;
	private DomainModelConvention<Object> domainModelConvention = null ; 
	
	
	public ModifyingOperationsState(Date serverStartDate, int maxOperationsToKeep, DomainModelConvention<Object> domainModelConvention) {
		this.serverStartTime = serverStartDate.getTime();
		this.maxOperationsToKeep = maxOperationsToKeep;
		this.domainModelConvention = domainModelConvention;
	}


	/**
	 * @param dirtyInfo
	 * @throws DatabaseException
	 */
	public synchronized boolean validateChanges(DispatchedModifyingMethodsTransport[] dirtyInfo) throws DatabaseException {
		for (DispatchedModifyingMethodsTransport transport : dirtyInfo) {
			if (!checkIfValidChange(transport)) {
				return false;
			}
		}
		registerChanges(dirtyInfo);
		return true;
	}
	
	
	private void registerChanges(DispatchedModifyingMethodsTransport[] dirtyInfo){
		long now = System.currentTimeMillis();
		for (DispatchedModifyingMethodsTransport dmt : dirtyInfo) {
			for (DispatchEntry entry : dmt.getMultipleDependentMethods()) {
				registerEntry(dmt.getTargetConcept(),entry,now);
			}
			for (DispatchEntry entry : dmt.getNonDependentMethods()) {
				registerEntry(dmt.getTargetConcept(),entry,now);
			}
			for (DispatchEntry entry : dmt.getSingleDependentMethods()) {
				registerEntry(dmt.getTargetConcept(),entry,now);
			}
		}
	}

	private void registerEntry(Object ejbInstance, DispatchEntry entry, long time){
		if (maxOperationsToKeep < modifyingMethods.size()) {
			String keyToDelete = modifyingMethodsQueue.poll();
			modifyingMethods.remove(keyToDelete);
		}
		String key = createModifiedMethodKey(ejbInstance, entry);
		modifyingMethods.put(key, time);
		modifyingMethodsQueue.offer(key);
	}
	
	private String createModifiedMethodKey(Object ejbInstance, DispatchEntry entry) {
		return ejbInstance.getClass().getName() + "." + ReflectionHelper.invokeMethod(ejbInstance, domainModelConvention.getIdentifierMethod(ejbInstance)) + "." + entry.getMethod().getName();
	}
	

	/**
	 * Checks to see if the changes are not based on stale data.
	 * @param dmmt
	 * @return true if the data is not stale, and false otherwise
	 */
	private boolean checkIfValidChange(DispatchedModifyingMethodsTransport dmmt){
		if (null == ReflectionHelper.invokeMethod(dmmt.getTargetConcept(), domainModelConvention.getIdentifierMethod(dmmt.getTargetConcept()))) {
			return true;
		}
		Object target = dmmt.getTargetConcept();
		long time = dmmt.getTimeOfRetrieval();
		if (!areEntriesValid(dmmt.getMultipleDependentMethods(),target,time)) {
			return false;
		}
		if (!areEntriesValid(dmmt.getNonDependentMethods(),target,time)) {
			return false;
		}
		if (!areEntriesValid(dmmt.getSingleDependentMethods(),target,time)) {
			return false;
		}
		return true;
	}

	private boolean areEntriesValid(Collection<DispatchEntry> dispatchEntries, Object instance, Long timeOfRetrieval) {
		for (DispatchEntry entry : dispatchEntries) {
			String key = createModifiedMethodKey(instance,entry);
			Long timeOfLastSave = modifyingMethods.get(key);
			boolean isServerRestartedSinceRetrieve = serverStartTime > timeOfRetrieval;
			boolean isSavedAfterRetrieve = (null != timeOfLastSave) && (timeOfLastSave > timeOfRetrieval);
			if (isServerRestartedSinceRetrieve || isSavedAfterRetrieve) {
				String target = instance.getClass().getName();
				log.warn("The data that was tried to be saved was stale. It has been changed since the client read it. No changes saved! target=" + target + "\nisSavedAfterRetrieve=" + isSavedAfterRetrieve + "   isServerRestartedSinceRetrieve=" + isServerRestartedSinceRetrieve);
				return false;
			}
		}
		return true;
	}
	
}
	
